#include "GL/glew.h"
#include "LoadShader.h"
#include "utility.h"
#include "vmath.h"
#include "GLFW/glfw3.h"
#include <functional>

class ObjectModel
{
public:
	Model3D model;
	Matrix4f toWorld;
	Vector4f color;
	std::function<void()> beforeDraw = nullptr;

	Matrix3f getNormalMatrix()
	{
		return Matrix3f::fromMat4<float>((float*)toWorld).inverse().transpose();
	}
};

class GLProgram
{
public:
	GLuint program = 0;
	std::function<void(ObjectModel&, Matrix4f&)> drawObject = nullptr;
	void DrawObject(ObjectModel& obj, Matrix4f& additional = Matrix4f())
	{
		glUseProgram(program);
		if (nullptr != obj.beforeDraw)
		{
			obj.beforeDraw();
		}
		drawObject(obj, additional);
		glUseProgram(0);
	}
};

GLProgram _program, _programOutLine;

const GLfloat outlineColor[4]{1.0f, 1.0f, 1.0f, 1.0f};

ObjectModel _pyramid1, _pyramid2, _cube, _ball, _groud;

float _eyeEngel = 0, _eyeAspect = 300, _radius = 800;
Matrix4f _view, _projective;
Vector3f eyePosition;

static bool IsLeftbuttonDown = false;
static double PreMousePosX(0), PreMousePosY(0);
void mousePositionCallback(GLFWwindow* window, double xpos, double ypos)
{
	if (IsLeftbuttonDown)
	{
		int state = glfwGetKey(window, GLFW_KEY_LEFT_CONTROL);
		if (state == GLFW_PRESS)
		{
			float newAspect = _eyeAspect + 2.0f*(float)(ypos - PreMousePosY);
			if (newAspect > -100.f && newAspect < 800.f)
			{
				_eyeAspect = newAspect;
			}
		}
		else
		{
			float newRadius = _radius + 6.0f*(float)(ypos - PreMousePosY);
			if (newRadius > 100 && newRadius < 2500)
			{
				_radius = newRadius;
			}
			_eyeEngel -= (float)(xpos - PreMousePosX) * 0.5f;
		}
	}
	PreMousePosX = xpos;
	PreMousePosY = ypos;
}

void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods)
{
	if (button == GLFW_MOUSE_BUTTON_LEFT)
	{
		IsLeftbuttonDown = action == GLFW_PRESS;
	}
}

bool init()
{
	_pyramid1.model = LoadPyramid(0, 1);
	_pyramid1.toWorld = Matrix4f::createTranslation(-60, 100.0f, 80) * Matrix4f::createScale(0.8f, 0.8f, 0.8f);
	_pyramid1.color = Vector4f(0.0f, 0.7f, 0.8f, 1.0f);

	_pyramid2.model = _pyramid1.model;
	_pyramid2.toWorld = Matrix4f::createTranslation(290, 120.0f, -80);
	_pyramid2.color = Vector4f(0.6f, 0.2f, 0.8f, 1.0f);

	_cube.model = LoadCube(0, 1);
	_cube.toWorld = Matrix4f::createTranslation(100, 70.0f, 0) * Matrix4f::createScale(0.5f, 0.5f, 0.5f);
	_cube.color = Vector4f(0.6f, 0.2f, 0.8f, 1.0f);

	_ball.model = LoadBall(0, 1);
	_ball.toWorld = Matrix4f::createTranslation(-200, 90.0f, 180);
	_ball.color = Vector4f(0.3f, 0.6f, 0.8f, 1.0f);

	_groud.model = LoadPanel(0, 1, -1);
	_groud.toWorld = Matrix4f::createRotationAroundAxis(90, 0, 0)* Matrix4f::createScale(4, 4, 4);
	_groud.color = Vector4f(0.2f, 0.5f, 0.3f, 1.0f);

	ShaderInfo si[] = { { GL_VERTEX_SHADER, "Shader.vert" },{ GL_FRAGMENT_SHADER, "Shader.frag" },{ GL_NONE, nullptr } };
	_program.program = LoadShade(si);
	_program.drawObject = [](ObjectModel& obj, Matrix4f& additional)
	{
		Matrix4f m = additional*obj.toWorld;
		Matrix3f n = Matrix3f::fromMat4<float>((float*)m).inverse().transpose();
		glUniformMatrix4fv(0, 1, GL_FALSE, m);
		glUniformMatrix4fv(1, 1, GL_FALSE, _projective*_view*m);
		glUniformMatrix3fv(3, 1, GL_FALSE, n);
		glUniform3fv(4, 1, eyePosition);
		glUniform4fv(100, 1, obj.color);
		obj.model.Draw();
	};

	ShaderInfo si2[] = { { GL_VERTEX_SHADER, "Outline.vert" },{ GL_FRAGMENT_SHADER, "Outline.frag" },{ GL_NONE, nullptr } };
	_programOutLine.program = LoadShade(si2);
	_programOutLine.drawObject = [](ObjectModel& obj, Matrix4f& additional)
	{
		Matrix4f m = additional*obj.toWorld;
		Matrix3f n = Matrix3f::fromMat4<float>((float*)m).inverse().transpose();
		glUniformMatrix4fv(0, 1, GL_FALSE, m);
		glUniformMatrix4fv(1, 1, GL_FALSE, _projective*_view*m);
		glUniformMatrix3fv(3, 1, GL_FALSE, n);
		glUniform3fv(4, 1, eyePosition);
		glUniform4fv(100, 1, outlineColor);
		obj.model.Draw();
	};


	return true;
}

void Display(double time)
{
	glClearColor(0.0f, 0.05f, 0.05f, 1.0);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
	glEnable(GL_DEPTH_TEST);


	float posX = _radius*sin(DEG2RAD_F(_eyeEngel)), posZ = _radius*cos((DEG2RAD_F(_eyeEngel)));
	eyePosition = Vector3f(posX, _eyeAspect, posZ);
	_view = Matrix4f::createLookAt(eyePosition, Vector3f(0.f, 0.f, 0.f), Vector3f(0.f, 1.f, 0.f));
	float ratioH_W = (float)getViewHeight() / (float)getViewWidth();
	_projective = Matrix4f::createFrustum(-5, 5, -5 * ratioH_W, 5 * ratioH_W, 6, 4000);
	_program.DrawObject(_groud);

	glClearStencil(0x0);
	glEnable(GL_STENCIL_TEST);

	ObjectModel *objs[] = {&_ball, &_cube, &_pyramid2, &_pyramid1};

	for each (auto o in objs)
	{
		glClearStencil(0xFF);
		glStencilFunc(GL_ALWAYS, 0x01, 0xFF);
		glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
		_program.DrawObject(*o);

		glStencilFunc(GL_NOTEQUAL, 0x01, 0xFF);
		glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
		_programOutLine.DrawObject(*o);

	}

	glDisable(GL_STENCIL_TEST);

}

void onClose()
{
	_pyramid1.model.deleteBuffer();
	_cube.model.deleteBuffer();
	_ball.model.deleteBuffer();
	_groud.model.deleteBuffer();
}

